/*****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations
 * under the License.
 ****************************************************************/
package reco.db;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.unmodifiableIterable;
import static com.google.common.collect.Lists.newArrayList;

import java.util.ArrayList;

/**
 * Describes the fields known at compiletime of a class.
 * <p>
 * Each field knows its type (java class allowed for its values), its name, the domain where is declared.
 * <p>
 * It also define, by its type, which operators can be used to filter {@link Domain}'s instances. No info is holded to define how it is mapped on a
 * database.
 * </p>
 * 
 * @param <T>
 *            field's type.
 */
@SuppressWarnings("serial")
public abstract class Field<T, D extends Domain<D>> implements java.io.Serializable {

    private final Class<T> type;
    private String name;
    private Class<D> domain;
    private T defaultValue;
    private final ArrayList<Constraint<T>> constraints;
    private boolean definitionComplete;

    /**
     * Creates a field defining only its type, and leaving its definition open.
     * <p>
     * After the field construction is done, the client <em>must</em> define the field's name and domain, and <em>can</em> add constraints to the
     * field.
     * 
     * @param fieldType
     *            Type of field's possible values.
     * @param fieldConstraintsDefinitions
     *            Definitions of {@link Constraint}'s to be atacched to the created field during construction.
     * @throws NullPointerException
     *             if <code>fieldType</code> is <code>null</code>.
     * 
     * @see #setName(String)
     * @see #setDomain(Class)
     */
    protected Field(Class<T> fieldType, ConstraintFactory... fieldConstraintsDefinitions) {
        this.type = checkNotNull(fieldType, "fieldType");
        this.definitionComplete = false;
        this.constraints = newArrayList();

        if (fieldConstraintsDefinitions != null) {
            for (final ConstraintFactory fieldConstraintDefinition : fieldConstraintsDefinitions) {
                addConstraint(fieldConstraintDefinition);
            }
        }
    }

    /**
     * Creates a field defining its type, name and domain, and leaving its definition open.
     * <p>
     * After the field construction is done, the client <em>can</em> define the field's name and domain, and <em>can</em> add constraints to the
     * field.
     * 
     * @param fieldType
     *            Type of field's possible values.
     * @param fieldName
     *            Name used to recognize the field in the domain.
     * @param fieldDomain
     *            Domain where the field is defined.
     * @param fieldConstraintsDefinitions
     *            Definitions of {@link Constraint}'s to be atacched to the created field during construction.
     * @throws NullPointerException
     *             if <code>fieldType</code> is <code>null</code>.
     * @throws NullPointerException
     *             if <code>fieldName</code> is <code>null</code>.
     * @throws NullPointerException
     *             if <code>fieldDomain</code> is <code>null</code>.
     */
    protected Field(Class<T> fieldType, String fieldName, Class<D> fieldDomain, ConstraintFactory... fieldConstraintsDefinitions) {
        this.name = checkNotNull(fieldName, "fieldName");
        this.type = checkNotNull(fieldType, "fieldType");
        this.domain = checkNotNull(fieldDomain, "fieldDomain");
        this.definitionComplete = false;
        this.constraints = newArrayList();

        if (fieldConstraintsDefinitions != null) {
            for (final ConstraintFactory fieldConstraintDefinition : fieldConstraintsDefinitions) {
                addConstraint(fieldConstraintDefinition);
            }
        }
    }

    /**
     * Gets the root {@link Domain} where the field is known, in other words, the {@link Domain} where it was declared and then it can be used.
     * <p>
     * This method would <em>never</em> return <code>null</code>.
     * 
     * @return the {@link Domain} where it was declared.
     */
    public Class<D> getDomain() {
        return this.domain;
    }

    /**
     * Gets the java {@link Class} allowed as values of the field.
     * <p>
     * This method would <em>never</em> return <code>null</code>.
     * 
     * @return the type of the {@link Field}.
     */
    public Class<T> getType() {
        return this.type;
    }

    /**
     * This value can't be <code>blank</code> and should not have any blanks on it, only java allowed identifiers should be returned by this.
     * 
     * @return the name of the field.
     */
    public String getName() {
        return this.name;
    }

    /**
     * Gets the value adopted by this field when it is not defined by the user.
     * <p>
     * <em>Note:</em> this value is not checked against any constraint of the field, so it can be an illegal value.
     * 
     * @return a value used as default on the field.
     */
    public T getDefaultValue() {
        return this.defaultValue;
    }

    /**
     * Constraints to be applyed on the field values.
     * <p>
     * This should not be <em>never</em> <code>null</code>, at most an emty list.
     * 
     * @return an unmodifiable iterable on the constraints defined on the field.
     */
    public Iterable<Constraint<T>> constraints() {
        return unmodifiableIterable(this.constraints);
    }

    /**
     * Adds a constraint to the field based on a definition holded by a constraint factory.
     * 
     * @param constraintDefinition
     *            The policy to be used to create the constraint to attach to the field.
     * 
     * @throws IllegalStateException
     *             if the field has its definition marked as completed.
     * @throws NullPointerException
     *             if constraint definition is <code>null</code>.
     * @throws NullPointerException
     *             if constraint definition creates a null <code>null</code> constraint.
     * @see #definitionCompleted()
     */
    public void addConstraint(ConstraintFactory constraintDefinition) {
        checkState(!this.definitionComplete, "%s definition was already setted as completed", getName());
        checkNotNull(constraintDefinition, "constraintDefinition");

        final Constraint<T> constraint = constraintDefinition.createConstraintFor(this);
        if (constraint != null) {
            this.constraints.add(constraint);
        }
    }

    /**
     * Defines field name.
     * 
     * @param name
     *            the name to set on the field.
     * @throws IllegalStateException
     *             if the field has its definition marked as completed.
     * @throws NullPointerException
     *             if <code>name</code> is <code>null</code>.
     * @see #definitionCompleted()
     */
    public void setName(String name) {
        checkState(!this.definitionComplete, "%s definition was already setted as completed", getName());
        this.name = checkNotNull(name, "name");
    }

    /**
     * Defines field domain.
     * 
     * @param domain
     *            the name to set on the field.
     * @throws IllegalStateException
     *             if the field has its definition marked as completed.
     * @throws NullPointerException
     *             if <code>domain</code> is <code>null</code>.
     * @see #definitionCompleted()
     */
    public void setDomain(Class<D> domain) {
        checkState(!this.definitionComplete, "%s definition was already setted as completed", getName());
        this.domain = checkNotNull(domain, "domain");
    }

    /**
     * Defines field default value.
     * 
     * @param defaultValue
     *            the default value to set on the field.
     * @throws IllegalStateException
     *             if the field has its definition marked as completed.
     * @see #definitionCompleted()
     */
    public void setDefaultValue(T defaultValue) {
        checkState(!this.definitionComplete, "%s definition was already setted as completed", getName());
        this.defaultValue = defaultValue;
    }

    /**
     * Gets the field's value on a {@link Domain}'s instance.
     * 
     * @param domain
     *            {@link Domain} where field's value must be extracted.
     * @return the value of the field on the domain.
     * @throws NullPointerException
     *             if domain is <code>null</code>.
     * @throws IllegalArgumentException
     *             if domain is not assignable to the class defined by {@link #getDomain()}.
     */
    public T of(D domain) {
        checkNotNull(domain, "domain");
        @SuppressWarnings("unchecked")
        // we can safely assume fields type
        final T value = (T) domain.readProperty(getName());
        return value;
    }

    /**
     * Marks de field definition as completed.
     * <p>
     * After this method is invoked, no field modifications are allowed.
     * 
     * @throws IllegalStateException
     *             if the field doesn't have a name.
     * @throws IllegalStateException
     *             if the field doesn't have a domain.
     * @throws IllegalStateException
     *             if the field has a name and a domain, but already has its definition marked as completed.
     */
    public void definitionCompleted() {
        checkState(this.name != null, "Field definition can not be completed if field does not have a name");
        checkState(this.domain != null, "%s definition can not be completed if field does not have a domain", getName());
        checkState(!this.definitionComplete, "%s definition was already setted as completed", getName());

        this.definitionComplete = true;
        this.constraints.trimToSize();
    }
}
